class EventBus {
  constructor() {
    this.eventMap = {};
  }

  on(key, callback, thisArg = null) {
    this.isKeyToString(key);
    this.isFunction(callback);

    const list = this.eventMap[key] || [];
    list.push({
      callback,
      thisArg,
    });
    this.eventMap[key] = list;
  }

  once(key, callback, thisArg = null) {
    this.isKeyToString(key);
    this.isFunction(callback);
    const fn = (...args) => {
      this.off(key, fn);
      callback.apply(thisArg, args);
    };
    this.on(key, fn, thisArg);
  }

  emit(key, ...args) {
    this.isKeyToString(key);
    const keys = Object.keys(this.eventMap);
    if (!keys.includes(key)) {
      return;
    }
    const list = this.eventMap[key] || [];

    list.forEach(({ callback, thisArg }) => {
      callback.apply(thisArg, args);
    });
  }

  off(key, callback) {
    this.isKeyToString(key);
    this.isFunction(callback);

    const list = this.eventMap[key] || [];
    const index = list.findIndex(({ callback: cb }) => cb === callback);
    if (index === -1) {
      return;
    }

    list.splice(index, 1);
    if (list.length === 0) {
      delete this.eventMap[key];
    }
  }

  clear() {
    this.eventMap = {};
  }

  hasEvent(key) {
    this.isKeyToString(key);
    return !!this.eventMap[key];
  }

  isKeyToString(key) {
    if (typeof key !== "string") {
      throw new TypeError("key must be a string");
    }
  }

  isFunction(fn) {
    if (typeof fn !== "function") {
      throw new TypeError("fn must be a function");
    }
  }
}

module.exports = EventBus;
